package dev.detekt.rules.style

import dev.detekt.api.Config
import dev.detekt.api.Entity
import dev.detekt.api.Finding
import dev.detekt.api.RequiresAnalysisApi
import dev.detekt.api.Rule
import dev.detekt.psi.firstParameterOrNull
import dev.detekt.psi.isCalling
import dev.detekt.psi.receiverIsUsed
import org.jetbrains.kotlin.analysis.api.analyze
import org.jetbrains.kotlin.idea.references.mainReference
import org.jetbrains.kotlin.name.CallableId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtLambdaExpression
import org.jetbrains.kotlin.psi.KtNamedDeclaration
import org.jetbrains.kotlin.psi.KtQualifiedExpression
import org.jetbrains.kotlin.psi.KtSafeQualifiedExpression
import org.jetbrains.kotlin.psi.KtSimpleNameExpression
import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType

/**
 * `let` expressions are used extensively in our code for null-checking and chaining functions,
 * but sometimes their usage should be replaced with an ordinary method/extension function call
 * to reduce visual complexity.
 *
 * <noncompliant>
 * a.let { print(it) } // can be replaced with `print(a)`
 * a.let { it.plus(1) } // can be replaced with `a.plus(1)`
 * a?.let { it.plus(1) } // can be replaced with `a?.plus(1)`
 * a?.let { that -> that.plus(1) }?.let { it.plus(1) } // can be replaced with `a?.plus(1)?.plus(1)`
 * a.let { 1.plus(1) } // can be replaced with `1.plus(1)`
 * a?.let { 1.plus(1) } // can be replaced with `if (a != null) 1.plus(1)`
 * </noncompliant>
 *
 * <compliant>
 * a?.let { print(it) }
 * a?.let { 1.plus(it) } ?.let { msg -> print(msg) }
 * a?.let { it.plus(it) }
 * val b = a?.let { 1.plus(1) }
 * </compliant>
 *
 */
class UnnecessaryLet(config: Config) :
    Rule(
        config,
        "The `let` usage is unnecessary."
    ),
    RequiresAnalysisApi {

    override fun visitCallExpression(expression: KtCallExpression) {
        super.visitCallExpression(expression)

        if (!expression.isCalling(letCallableId)) return
        val lambdaExpr = expression.lambdaArguments.firstOrNull()?.getLambdaExpression()

        val referenceCount = lambdaExpr?.countLambdaParameterReference() ?: 0
        if (referenceCount > 1) return

        if (expression.parent is KtSafeQualifiedExpression) {
            if (lambdaExpr != null) {
                when {
                    referenceCount == 0 && !expression.receiverIsUsed() ->
                        report(expression, "let expression can be replaced with a simple if")

                    referenceCount == 1 && canBeReplacedWithCall(lambdaExpr) ->
                        report(expression, "let expression can be omitted")
                }
            }
        } else {
            if (referenceCount == 0 || canBeReplacedWithCall(lambdaExpr) || !expression.inCallChains()) {
                report(expression, "let expression can be omitted")
            }
        }
    }

    private fun report(expression: KtCallExpression, message: String) {
        report(Finding(Entity.from(expression), message))
    }

    companion object {
        private val letCallableId = CallableId(FqName("kotlin"), Name.identifier("let"))
    }
}

private fun KtCallExpression.inCallChains(): Boolean {
    val qualified = parent as? KtQualifiedExpression ?: return false
    return qualified.parent is KtQualifiedExpression || qualified.receiverExpression is KtQualifiedExpression
}

private fun canBeReplacedWithCall(lambdaExpr: KtLambdaExpression?): Boolean {
    if (lambdaExpr == null) return false

    val receiver = when (val statement = lambdaExpr.bodyExpression?.statements?.singleOrNull()) {
        is KtQualifiedExpression -> statement.getRootExpression()
        else -> null
    } ?: return false

    val lambdaParameter = lambdaExpr.valueParameters.singleOrNull()
    val lambdaParameterNames = if (lambdaParameter == null) {
        listOf(Name.identifier("it"))
    } else {
        lambdaParameter.destructuringDeclaration?.entries.orEmpty()
            .plus(lambdaParameter)
            .filterIsInstance<KtNamedDeclaration>()
            .map { it.nameAsSafeName }
    }
    return lambdaParameterNames.any { receiver.textMatches(it.asString()) }
}

private fun KtExpression?.getRootExpression(): KtExpression? {
    // Look for the expression that was the root of a potential call chain.
    var receiverExpression = this
    while (receiverExpression is KtQualifiedExpression) {
        receiverExpression = receiverExpression.receiverExpression
    }
    return receiverExpression
}

private fun KtLambdaExpression.countLambdaParameterReference(): Int {
    val bodyExpression = bodyExpression ?: return 0
    val variableList = if (valueParameters.isNotEmpty()) {
        valueParameters.first().destructuringDeclaration?.let {
            analyze(it) {
                it.symbol.entries
            }
        } ?: listOf(firstParameterOrNull())
    } else {
        // implicit it param
        listOf(firstParameterOrNull())
    }.filterNotNull()

    return variableList.sumOf { variableSymbol ->
        bodyExpression.collectDescendantsOfType<KtSimpleNameExpression> {
            analyze(it) {
                it.mainReference.resolveToSymbol() == variableSymbol
            }
        }.count()
    }
}
